package com.hay.elasticsearch.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hay.common.PageResult;
import com.hay.elasticsearch.client.BrandClient;
import com.hay.elasticsearch.client.CategoryClient;
import com.hay.elasticsearch.client.GoodsClient;
import com.hay.elasticsearch.client.SpecificationClient;
import com.hay.elasticsearch.pojo.Goods;
import com.hay.elasticsearch.pojo.SearchRequest;
import com.hay.elasticsearch.pojo.SearchResult;
import com.hay.elasticsearch.repository.GoodsReponsitory;
import com.hay.item.pojo.*;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @title: SearchService
 * @Author HuangYan
 * @Date: 2020/7/26 20:10
 * @Version 1.0
 */
@Service
public class SearchService {


    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private SpecificationClient specificationClient;

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private GoodsReponsitory goodsReponsitory;

    private static final ObjectMapper MAPPER = new ObjectMapper();


    /**
     * 关键字分页查询数据
     * @param searchRequest
     * @return
     */
    public SearchResult searchByPage(SearchRequest searchRequest) {
        // 如果搜索字段为空，则返回空
        if (StringUtils.isBlank(searchRequest.getKey())){
            return null;
        }
        // 创建自动有搜索
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        // 设置查询条件为匹配查询match
        //MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("all", searchRequest.getKey()).operator(Operator.AND);
        // 当存在规格参数过滤查询时，需要使用布尔查询
        BoolQueryBuilder boolQueryBuilder = buildBoolQueryBuilder(searchRequest);
        queryBuilder.withQuery(boolQueryBuilder);
        // 设置结果集过滤
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","skus","subTitle"},null));
        // 设置分页
        queryBuilder.withPageable(PageRequest.of(searchRequest.getPage() - 1, searchRequest.getSize()));

        // 添加聚合查询
        String categoryAggName = "categoriesAgg";
        String brandsAggName = "brandsAgg";
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
        queryBuilder.addAggregation(AggregationBuilders.terms(brandsAggName).field("brandId"));

        // 执行查询
        AggregatedPage<Goods> goods = (AggregatedPage<Goods>)goodsReponsitory.search(queryBuilder.build());
        // 获取到商品类别category聚合结果,并对其中的桶进行解析
        Aggregation categoryAgg = goods.getAggregation(categoryAggName);
        List<Map<String, Object>> categories = getCategoryAggResult(categoryAgg);
        // 获取到商品brand聚合结果
        Aggregation brandAgg = goods.getAggregation(brandsAggName);
        List<Brand> brands = getBrandAggResult(brandAgg);

        // 查询规格参数聚合
        List<Map<String, Object>> specs = new ArrayList<>();
        if (!CollectionUtils.isEmpty(categories) && categories.size() == 1){
            // 只有当商品种类确定为一个时才显示此商品的规格参数
            specs = getParamAggResult((Long)categories.get(0).get("id"), boolQueryBuilder);
        }

        // 分页封装，并返回
        return new SearchResult(goods.getTotalElements(), goods.getTotalPages(), goods.getContent(),categories,brands,specs);

    }

    /**
     * 构建过滤查询
     * @param searchRequest
     * @return
     */
    private BoolQueryBuilder buildBoolQueryBuilder(SearchRequest searchRequest) {
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        // 构建匹配查询
        boolQueryBuilder.must(QueryBuilders.matchQuery("all", searchRequest.getKey()).operator(Operator.AND));
        // 构建过滤查询
        Map<String, String> filter = searchRequest.getFilter();
        for (Map.Entry<String, String> entry : filter.entrySet()) {
            // 判断过滤的字段，当为分类时:cid3；当为品牌时:brandId;当为specs时...
            String field = entry.getKey();
            if ("分类".equals(field)){
                field = "cid3";
            } else if ("品牌".equals(field)){
                field = "brandId";
            } else {
                field = "specs." + field + ".keyword";
            }
            // 构建过滤查询
            boolQueryBuilder.filter(QueryBuilders.termQuery(field, entry.getValue()));
        }
        return boolQueryBuilder;
    }

    /**
     * 解析规格参数
     * @param cid
     * @param boolQueryBuilder
     * @return
     */
    private List<Map<String, Object>> getParamAggResult(Long cid, BoolQueryBuilder boolQueryBuilder) {

        // 根据cid查询此商品分类的所有规格参数
        List<SpecParam> specParams = specificationClient.queryParams(null, cid, null, true);
        // 将所有的规格参数进行聚合查询
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        queryBuilder.withQuery(boolQueryBuilder);
        // 过滤所有的聚合结果
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{},null));
        specParams.forEach(param -> {
            // 将所有的规格参数进行聚合查询
            // if (param.getName() != "内存")
            queryBuilder.addAggregation(AggregationBuilders.terms(param.getName())
                    .field("specs." + param.getName() + ".keyword"));

        });
        AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)goodsReponsitory.search(queryBuilder.build());

        // 定义一个集合，收集聚合结果集
        List<Map<String, Object>> paramMapList = new ArrayList<>();
        // 解析聚合查询结果
        Map<String, Aggregation> aggregationMap = goodsPage.getAggregations().asMap();
        // 遍历集合
        for (Map.Entry<String, Aggregation> entry : aggregationMap.entrySet()) {
            Map<String, Object> map = new HashMap<>();
            // 将规格参数的名称加入map中
            map.put("key", entry.getKey());
            // 解析聚合，将聚合桶中所有的key组合成一个list
            List<Object> options = new ArrayList<>();
            StringTerms terms = (StringTerms) entry.getValue();
            terms.getBuckets().forEach(bucket -> {
                options.add(bucket.getKeyAsString());
            });
            // 将聚合桶中所有的key组合成的list放入map中
            map.put("options", options);
            // 将map放入规格参数集合
            paramMapList.add(map);
        }
        return paramMapList;
    }

    /**
     * 解析品牌聚合结果
     * @param brandAgg
     * @return
     */
    private List<Brand> getBrandAggResult(Aggregation brandAgg) {
        LongTerms terms = (LongTerms) brandAgg;
        // 遍历桶中数据，再根据bid查询brand
        return terms.getBuckets().stream().map(bucket -> {
            return brandClient.queryBrandById(bucket.getKeyAsNumber().longValue());
        }).collect(Collectors.toList());
    }

    /**
     * 解析商品类别category聚合结果
     * @param categoryAgg
     * @return
     */
    private List<Map<String, Object>> getCategoryAggResult(Aggregation categoryAgg) {

        LongTerms terms = (LongTerms)categoryAgg;
        // 将每个桶中的商品编号和商品名称存入集合中返回
        return terms.getBuckets().stream().map(bucket -> {
            Map<String, Object> map = new HashMap<>();
            // 根据cid查询出商品类别的名称
            List<String> categoryName = categoryClient.queryNamesByIds(Arrays.asList(bucket.getKeyAsNumber().longValue()));
            map.put("id", bucket.getKey());
            map.put("name", categoryName.get(0));
            return map;
        }).collect(Collectors.toList());

    }


    public Goods buildGoods(Spu spu) throws JsonProcessingException {

        // 创建一个Goods对象
        Goods goods = new Goods();

        // 获取分类字符串
        List<String> categoryNames = categoryClient.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
        // 获取品牌
        Brand brand = brandClient.queryBrandById(spu.getBrandId());
        // 获取此品牌的所有sku
        List<Sku> skus = goodsClient.querySkuListBySpuId(spu.getId());
        // 用于存储所有价格
        List<Long> price = new ArrayList<>();
        // 用于存储各sku属性的集合
        List<Map<String, Object>> skuMapList = new ArrayList<>();
        skus.forEach(sku -> {
            // 此品牌下的所有商品价格进行收集
            Long skuPrice = sku.getPrice();
            price.add(skuPrice);

            Map<String, Object> skuMap = new HashMap<>();
            skuMap.put("id", sku.getId());
            skuMap.put("title", sku.getTitle());
            skuMap.put("price", sku.getPrice());
            // 图片有多张时，取第一张
            skuMap.put("image", StringUtils.split(sku.getImages(), ",")[0]);
            skuMapList.add(skuMap);
        });

        // 用于存储规格参数
        Map<String, Object> specs = new HashMap<>();
        // 根据cid3查询所有的规格参数
        List<SpecParam> specParams = specificationClient.queryParams(null, spu.getCid3(), null, true);
        // 查询spuDetail。获取规格参数值
        SpuDetail spuDetail = goodsClient.querySpuDetailBySpuId(spu.getId());
        // 获取通用的规格参数
        Map<String, Object> genericSpec = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<String, Object>>() {});
        // 获取特殊的规格参数
        Map<String, List<Object>> specialSpec = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<String, List<Object>>>() {});

        // 定义map接收{规格参数名，规格参数值}
        Map<String, Object> paramMap = new HashMap<>();
        specParams.forEach(param -> {
            // 判断是否通用规格参数
            if (param.getGeneric()) {
                // 获取通用规格参数值
                String value = genericSpec.get(param.getId().toString()).toString();
                // 判断是否是数值类型
                if (param.getNumeric()){
                    // 如果是数值的话，判断该数值落在那个区间
                    value = chooseSegment(value, param);
                }
                // 把参数名和值放入结果集中
                paramMap.put(param.getName(), value);
            } else {
                paramMap.put(param.getName(), specialSpec.get(param.getId().toString()));
            }
        });

        // 设置参数
        goods.setId(spu.getId());
        goods.setSubTitle(spu.getSubTitle());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setBrandId(spu.getBrandId());
        goods.setCreateTime(spu.getCreateTime());
        // 设置所有需要被搜索的信息，包含标题，分类，甚至品牌
        goods.setAll(spu.getTitle() + " " + StringUtils.join(categoryNames," ") + " " + brand.getName());
        // 设置价格
        goods.setPrice(price);
        // 设置Sku
        goods.setSkus(MAPPER.writeValueAsString(skuMapList));
        // 设置Specs规格参数
        goods.setSpecs(paramMap);

        return goods;
    }


    private String chooseSegment(String value, SpecParam p) {
        double val = NumberUtils.toDouble(value);
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = NumberUtils.toDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if(segs.length == 2){
                end = NumberUtils.toDouble(segs[1]);
            }
            // 判断是否在范围内
            if(val >= begin && val < end){
                if(segs.length == 1){
                    result = segs[0] + p.getUnit() + "以上";
                }else if(begin == 0){
                    result = segs[1] + p.getUnit() + "以下";
                }else{
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }
}
